package com.nightpiter.model;

import static com.nightpiter.controller.log.Logger.log;
import static com.nightpiter.controller.log.Logger.logException;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import android.graphics.PointF;
import android.os.Handler;

import com.nightpiter.NightPiter;
import com.nightpiter.exceptions.BuilderException;
import com.nightpiter.model.MetroModelState.Action;
import com.nightpiter.model.objects.Hub;
import com.nightpiter.model.objects.Line;
import com.nightpiter.model.objects.Station;
import com.nightpiter.model.objects.Transfer;
import com.nightpiter.model.objects.Tunnel;
import com.nightpiter.model.reader.MetroModelReaderFactory;

/**
 * MVC's Model class
 * 
 * to instantiate this class use MetroModelBuilder's build method
 * 
 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
 */
public class MetroModel {

	/**
	 * Application context
	 */
	private NightPiter context;

	/**
	 * the Model state
	 */
	MetroModelState state;

	/**
	 * the name of the city represented in this Model
	 */
	private String cityName = "";

	/**
	 * the underground stations
	 */
	private ArrayList<Station> stations = new ArrayList<Station>();

	/**
	 * the underground hubs
	 */
	private ArrayList<Hub> hubs = new ArrayList<Hub>();

	/**
	 * the underground lines
	 */
	private ArrayList<Line> lines = new ArrayList<Line>();

	/**
	 * the underground transfers
	 */
	private ArrayList<Transfer> transfers = new ArrayList<Transfer>();

	/**
	 * the underground tunnels
	 */
	private ArrayList<Tunnel> tunnels = new ArrayList<Tunnel>();

	/**
	 * this tree allow to quickly find station by it's name
	 */
	TreeMap<String, Station> stationsByNameIndex;

	/**
	 * this tree allow to quickly find station by it's position
	 */
	TreeMap<PointF, Station> stationsByPointXIndex;

	/**
	 * default constructor
	 * 
	 * @param context
	 *            the application context
	 */
	public MetroModel(NightPiter context) {
		this.context = context;
		log("Model instance created");
	}

	/**
	 * should be called after another MVC components created
	 */
	public void init() {
		log("Model init() call");
		this.state = new MetroModelState();
		this.stationsByNameIndex = new TreeMap<String, Station>(
				new Comparator<String>() {
					public int compare(String one, String two) {
						return one.compareToIgnoreCase(two);
					}
				});

		this.stationsByPointXIndex = new TreeMap<PointF, Station>(
				new Comparator<PointF>() {
					public int compare(PointF one, PointF two) {
						float x1 = one.x;
						float x2 = two.x;
						if (x1 != x2) {
							return (int) Math.signum(x1 - x2);
						} else {
							return (int) Math.signum(one.y - two.y);
						}
					}
				});
	}

	/**
	 * * build method
	 * 
	 * package protected (should be called from MetroModelBuilder's build
	 * method)
	 * 
	 * @param cityName
	 *            the name of the city represented in this Model
	 * @param stations
	 *            the underground stations
	 * @param transfers
	 *            the underground transfers
	 * @param tunnels
	 *            the underground tunnels
	 * @param hubs
	 *            the underground hubs ("linked" transfers)
	 * @param lines
	 *            the underground lines ("linked" tunnels)
	 */
	void build(String cityName, ArrayList<Station> stations,
			ArrayList<Transfer> transfers, ArrayList<Tunnel> tunnels,
			ArrayList<Hub> hubs, ArrayList<Line> lines) {
		this.cityName = cityName;
		this.stations = stations;
		this.transfers = transfers;
		this.tunnels = tunnels;
		this.hubs = hubs;
		this.lines = lines;

		for (Station station : stations) {
			this.stationsByNameIndex.put(station.getName(), station);
			this.stationsByPointXIndex.put(new PointF(station.getPosition()),
					station);
		}
	}

	/**
	 * loads a new model data
	 * 
	 * @param filePath
	 *            the file path; could be null - in this case the metro data
	 *            would be loaded from resources
	 */
	public void load(String filePath, Handler handler) {
		log("Preparing to read MetroModel");
		this.state.action = Action.LOADING;
		this.state.resetAll(handler);
		try {
			MetroModelReaderFactory.getReader(this).readFromFile(filePath,
					this.getContext());
		} catch (BuilderException e) {
			logException(e);
		}
		this.state.action = Action.IDLE;
		this.state.setReady(true);
		log("MetroModel read completed");
	}

	/**
	 * loads a new model data
	 * 
	 * @param resId
	 *            resource id to load data from
	 */
	public void load(int resId, Handler handler) {
		log("Preparing to read MetroModel");
		this.state.action = Action.LOADING;
		this.state.resetAll(handler);
		try {
			MetroModelReaderFactory.getReader(this).readFromResource(resId,
					this.getContext());
		} catch (BuilderException e) {
			logException(e);
		}
		this.state.action = Action.IDLE;
		this.state.setReady(true);
		log("MetroModel read completed");
	}

	/**
	 * search the station by point
	 * 
	 * @param point
	 * @return
	 */
	public Station searchStation(PointF point) {
		final int DELTA = 1;
		float x = point.x;
		float y = point.y;
		PointF pointDeltaMinus = new PointF(x - DELTA, y - DELTA);
		PointF pointDeltaPlus = new PointF(x + DELTA, y + DELTA);
		SortedMap<PointF, Station> subMap = this.stationsByPointXIndex.subMap(
				pointDeltaMinus, pointDeltaPlus);
		Set<PointF> points = subMap.keySet();

		float minDist = DELTA * 2;
		float dist = 0;
		PointF nearestPoint = null;
		for (PointF eachPoint : points) {
			dist = (float) Math.sqrt(Math.pow((float) (eachPoint.x - x), 2)
					+ Math.pow((float) (eachPoint.y - y), 2));
			if (dist < minDist) {
				minDist = dist;
				nearestPoint = eachPoint;
			}
		}

		if (nearestPoint != null) {
			return subMap.get(nearestPoint);
		} else {
			return null;
		}
	}

	/**
	 * @return the cityName
	 */
	public String getCityName() {
		return cityName;
	}

	/**
	 * @return the stations
	 */
	public ArrayList<Station> getStations() {
		return stations;
	}

	/**
	 * @return the transfers
	 */
	public ArrayList<Transfer> getTransfers() {
		return transfers;
	}

	/**
	 * @return the tunnels
	 */
	public ArrayList<Tunnel> getTunnels() {
		return tunnels;
	}

	/**
	 * @return the hubs
	 */
	public ArrayList<Hub> getHubs() {
		return hubs;
	}

	/**
	 * @return the lines
	 */
	public ArrayList<Line> getLines() {
		return lines;
	}

	/**
	 * @return the context
	 */
	protected NightPiter getContext() {
		return context;
	}

	/**
	 * @return the state
	 */
	public MetroModelState getState() {
		return state;
	}

}
